# -*- coding: utf-8 -*-

"""
Python wrapper for hotweb API (http://hotweb.bz/)

Author: kaneko@triax.jp
License: MIT
"""

import logging

import time
import datetime

from urllib import urlopen, urlencode
import lxml.etree

__all__ = ["HotwebError", "HotwebAPIError", "Hotweb"]

log = logging.getLogger(__name__)


def safe_text(node):
    if isinstance(node, (list, tuple)):
        if len(node) > 0:
            return node[0].text
        else:
            return None
    else:
        return node.text


class HotwebError(Exception):
    pass

class HotwebAPIError(HotwebError):
    def __init__(self, errors=None):
        super(HotwebAPIError, self).__init__(
            self.compound_error_messages(errors))
        self.errors = errors

    @staticmethod
    def compound_error_messages(errors):
        messages = []
        if errors:
            for error in errors:
                message = error.get("message")
                if message:
                    messages.append(message)
        return u"\n".join(messages)


class Hotweb(object):
    default_api_url = "http://api.hotweb.bz/api"
    demo_api_url    = "http://api.demo.hotweb.bz/api"

    def __init__(self, api_key, api_url=None, demo=False, cache=None):
        """
        """
        self.api_key = api_key
        if api_url:
            self.api_url = api_url
        elif demo:
            self.api_url = self.demo_api_url
        else:
            self.api_url = self.default_api_url
        self.cache = cache

    def PageSearch(self, Keywords=None, Tags=None, MainCategoryId=None,
                   CategoryId=None, CreateUserId=None, PageThumbnailSize=None,
                   PagingNumber=None, PagingUnit=None, Sort=None, Order=None,
                   CategoryThumbnailSize=None):
        """
        ページ検索

        検索結果について
        ・未登録の情報は、タグ自体表示されません。
        ・公開状態が、「公開」のページのみ取得できます。
        ・公開状態が、「非公開」のページは取得できません。

        オプションパラメータ

        Keywords

            検索対象に含まれる文字列を指定します。

            検索対象：
            ・ページのタイトル
            ・ページの説明
            ・ページのkeyword
            ・ページのdescription
            ・ページのタグ
            ・各コンポーネントのタイトル
            ・各コンポーネントのHTML

            ※指定した文字列に対して、部分一致したものを対象とします。
            ※英字の大文字小文字は区別しません。

        Tags

            ページタグに登録されている文字列を指定します。

            ※指定した文字列に対して、完全一致したものを対象とします。
            ※複数のタグを指定することはできません。

        MainCategoryId

            ページに指定したメインカテゴリのメインカテゴリIDを指定します。

        CategoryId

            ページに指定したカテゴリのカテゴリIDを指定します。

        CreateUserId

            ページを作成したユーザーのログインIDを指定します。

        PageThumbnailSize

            ページにサムネイルを登録している場合、サムネイルのサイズを
            指定することができます。

            デフォルト：100

            ※元のサイズの縦横比率で、指定したサイズに収まる範囲で縮小されます。

        PagingNumber

            ページ番号を指定します。
            デフォルト：1

        PagingUnit

            1ページあたりの取得件数を指定します。

            指定可能な値：10、20、30、50
            デフォルト：10

        Sort

            表示する順番を指定します。

            指定可能な値：PublishDate
            PublishDate：公開日付
            デフォルト：PublishDate

        Order

            Sortで指定した項目の順番の昇順、降順を指定します。

            指定可能な値：ASC、DESC
            ASC：昇順
            DESC：降順
            デフォルト：DESC

        CategoryThumbnailSize

            カテゴリにサムネイルを登録している場合、サムネイルのサイズ
            を指定することができます。

            デフォルト：100
        """

        args = {}

        if Keywords:
            if isinstance(Keywords, (list, tuple)):
                Keywords = ",".join(Keywords)
            args["Keywords"] = Keywords

        if Tags:
            args["Tags"] = Tags

        if MainCategoryId:
            args["MainCategoryId"] = MainCategoryId

        if CategoryId:
            args["CategoryId"] = CategoryId

        if CreateUserId:
            args["CreateUserId"] = CreateUserId

        if PageThumbnailSize:
            args["PageThumbnailSize"] = self.parse_size(PageThumbnailSize)

        if PagingNumber:
            args["PagingNumber"] = PagingNumber

        if PagingUnit:
            args["PagingUnit"] = PagingUnit

        if Sort:
            args["Sort"] = Sort

        if Order:
            args["Order"] = Order

        if CategoryThumbnailSize:
            args["ResponseItem"] = ("Category{CategoryThumbnailSize:%s}"
                                    % self.parse_size(CategoryThumbnailSize))

        result = self.call("PageSearch", **args)

        d = {
            "TotalResults": safe_text(result.xpath(
                    "/APIRequest/Pages/TotalResults")),
            "TotalPages": safe_text(result.xpath(
                    "/APIRequest/Pages/TotalPages")),
            }

        d["pages"] = []
        for page in result.xpath("/APIRequest/Pages/Page"):
            d["pages"].append(self.parse_page(page))
        return d

    def CategorySearch(self, Keywords=None, Tags=None, MainCategoryId=None,
                       CategoryId=None, CreateUserId=None,
                       CategoryThumbnailSize=None, PagingNumber=None,
                       PagingUnit=None, Sort=None, Order=None,
                       NoPagesOption=False):
        """
        カテゴリ検索

        検索結果について
        ・未登録の情報は、タグ自体表示されません。

        オプションパラメータ

        Keywords

            検索対象に含まれる文字列を指定します。

            検索対象：
            ・カテゴリ名
            ・カテゴリ説明

            ※指定した文字列に対して、部分一致したものを対象とします。
            ※英字の大文字小文字は区別しません。

        Tags

            カテゴリタグに登録されている文字列を指定します。

            ※指定した文字列に対して、完全一致したものを対象とします。
            ※複数のタグを指定することはできません。

        MainCategoryId

            メインカテゴリIDを指定します。

        CategoryId

            カテゴリIDを指定します。

        CreateUserId

            カテゴリを作成したユーザーのログインIDを指定します。

        CategoryThumbnailSize

            カテゴリにサムネイルを登録している場合、サムネイルのサイズ
            を指定することができます。

            デフォルト：100

        PagingNumber

            ページ番号を指定します。
            デフォルト：1

        PagingUnit

            1ページあたりの取得件数を指定します。

            指定可能な値：10、20、30、50
            デフォルト：10

        Sort

            表示する順番を指定します。

            指定可能な値：CreateDate、UpdateDate
            CreateDate：作成日付
            UpdateDate：更新日付
            デフォルト：CreateDate

        Order

            Sortで指定した項目の順番の昇順、降順を指定します。

            指定可能な値：ASC、DESC
            ASC：昇順
            DESC：降順
            デフォルト：DESC

        NoPagesOption

            ページがひとつもないカテゴリ情報を取得するかどうかを指定します。
            デフォルトは、ページがひとつもないカテゴリは検索対象外となります。
        """

        args = {}

        if Keywords:
            args["Keywords"] = Keywords

        if Tags:
            args["Tags"] = Tags

        if MainCategoryId:
            args["MainCategoryId"] = MainCategoryId

        if CategoryId:
            args["CategoryId"] = CategoryId

        if CreateUserId:
            args["CreateUserId"] = CreateUserId

        if CategoryThumbnailSize:
            args["CategoryThumbnailSize"] = self.parse_size(CategoryThumbnailSize)

        if PagingNumber:
            args["PagingNumber"] = PagingNumber

        if PagingUnit:
            args["PagingUnit"] = PagingUnit

        if Sort:
            args["Sort"] = Sort

        if Order:
            args["Order"] = Order

        if NoPagesOption:
            args["ResponseItem"] = "NoPagesOption"

        result = self.call("CategorySearch", **args)

        d = {
            "TotalResults": safe_text(result.xpath(
                    "/APIRequest/Categories/TotalResults")),
            "TotalPages": safe_text(result.xpath(
                    "/APIRequest/Categories/TotalPages")),
            }

        d["categories"] = []
        for page in result.xpath("/APIRequest/Categories/Category"):
            d["categories"].append(self.parse_category(page))
        return d

    def PageDetail(self, PageId, ContentType=None, PageThumbnailSize=None):
        """
        ページ詳細

        検索結果について
        ・未登録の情報は、タグ自体表示されません。
        ・公開状態が、「公開」のページのみ取得できます。
        ・公開状態が、「非公開」のページは取得できません。
        ・リリース時に指定した文字コードでページ情報を取得します。

        オプションパラメータ

        PageId

            取得したいページのページIDを指定します。

        ContentType

            ページ情報の取得形式を指定します。

            指定可能な値：url、iframe、all
            url：URL
            iframe：IFRAMEタグ
            all：両方
            デフォルト：url

            ※urlの場合は、ヘッダ情報を省いた<div class="hw-body">内だけ
              出力したファイルのパスとなります。
            ※iframeの場合は、タイトル、CSSなど全てを含んだHTMLを内部に
              表示するiframeタグとなります。

        PageThumbnailSize

            ページにサムネイルを登録している場合、サムネイルのサイズを
            指定することができます。

            デフォルト：100

        """

        args = {}

        args["PageId"] = PageId

        if ContentType:
            args["ContentType"] = ContentType

        if PageThumbnailSize:
            args["PageThumbnailSize"] = self.parse_size(PageThumbnailSize)

        args["ResponseItem"] = "Category"

        result = self.call("PageDetail", **args)

        pages = result.xpath("/APIRequest/Pages/Page")
        if not pages:
            raise HotwebAPIError

        return self.parse_page(pages[0])

    def call(self, operation, **kw):
        q = urlencode([("APIKey", self.api_key),
                       ("Operation", operation)] + kw.items())
        url = self.api_url + '?' + q

        def createfunc():
            f = urlopen(url)
            try:
                return f.read()
            finally:
                f.close()

        if self.cache:
            response = self.cache.get_value(url, createfunc=createfunc)
        else:
            response = createfunc()

        return self.parse_response(response)

    def parse_response(self, response):
        xml = lxml.etree.fromstring(response)

        status = xml.xpath("/APIRequest/ResponseStatus")
        if len(status) == 0:
            raise HotwebAPIError
        if status[0].text != 'OK':
            errors = []
            for error in xml.xpath("/APIRequest/Errors/Error"):
                errors.append({
                        "code":    safe_text(error.xpath("./Code")),
                        "message": safe_text(error.xpath("./Message")),
                        })
            raise HotwebAPIError(errors=errors)

        return xml

    def parse_size(self, size):
        if isinstance(size, int):
            return "%dx%d" % (size, size)
        else:
            return size

    def parse_datetime(self, dt):
        try:
            time_tuple = time.strptime(dt, "%Y-%m-%d %H:%M:%S")
        except ValueError:
            return None
        return datetime.datetime(*time_tuple[:6])

    def parse_page(self, node):
        d = dict(
            PageId        = safe_text(node.xpath("./PageId")),
            PageAlias     = safe_text(node.xpath("./PageAlias")),
            PageEncoding  = safe_text(node.xpath("./PageEncoding")),
            Title         = safe_text(node.xpath("./Title")),
            Summary       = safe_text(node.xpath("./Summary")),
            Keywords      = safe_text(node.xpath("./Keywords")),
            Description   = safe_text(node.xpath("./Description")),
            IFrameTag     = safe_text(node.xpath("./IFrameTag")),
            PageURL       = safe_text(node.xpath("./PageURL")),
            Tags          = safe_text(node.xpath("./Tags")),
            DisplayOption = dict(
                NewFlg  = safe_text(node.xpath("./DisplayOption/NewFlg")),
                ),
            CreateUser    = safe_text(node.xpath("./CreateUser")),
            UpdateUser    = safe_text(node.xpath("./UpdateUser")),

            PublicationDatetime = self.parse_datetime(
                safe_text(node.xpath("./PublicationDatetime"))),
            CreateDatetime      = self.parse_datetime(
                safe_text(node.xpath("./CreateDatetime"))),
            UpdateDatetime      = self.parse_datetime(
                safe_text(node.xpath("./UpdateDatetime"))),
            )

        if node.xpath("./Thumbnail"):
            d["Thumbnail"] = dict(
                ImageURL = safe_text(node.xpath("./Thumbnail/ImageURL")),
                Width    = safe_text(node.xpath("./Thumbnail/Width")),
                Height   = safe_text(node.xpath("./Thumbnail/Height")),
                )

        if node.xpath("./Stylesheets"):
            d["Stylesheets"] = [
                { "StylesheetURL": e.text }
                for e in node.xpath("./Stylesheets/StylesheetURL")]
        if node.xpath("./Javascript"):
            d["Javascript"]  = [
                { "JavascriptURL": e.text }
                for e in node.xpath("./Javascript/JavascriptURL")]

        category = node.xpath("./Category")
        if category:
            d["Category"] = self.parse_category(category[0])

        return d

    def parse_category(self, node):
        d = dict(
            CategoryId     = safe_text(node.xpath("./CategoryId")),
            CategoryAlias  = safe_text(node.xpath("./CategoryAlias")),
            CategoryName   = safe_text(node.xpath("./CategoryName")),
            ShortSummary   = safe_text(node.xpath("./ShortSummary")),
            LongSummary    = safe_text(node.xpath("./LongSummary")),
            Tags           = safe_text(node.xpath("./Tags")),
            TotalPageCount = safe_text(node.xpath("./TotalPageCount")),
            )

        if node.xpath("./Thumbnail"):
            d["Thumbnail"] = dict(
                ImageURL = safe_text(node.xpath("./Thumbnail/ImageURL")),
                Width    = safe_text(node.xpath("./Thumbnail/Width")),
                Height   = safe_text(node.xpath("./Thumbnail/Height")),
                )

        if node.xpath("./MainCategory"):
            d["MainCategory"] = dict(
                MainCategoryId = safe_text(node.xpath(
                        "./MainCategory/MainCategoryId")),
                MainCategoryName = safe_text(node.xpath(
                        "./MainCategory/MainCategoryName")),
                LongSummary = safe_text(node.xpath(
                        "./MainCategory/LongSummary")),
                )

        return d
